package msg;

import static java.lang.Math.*;

import java.text.MessageFormat;

import org.json.*;

import common.Pair;

/**
 * Represents a location using lat/long coordinates
 * 
 * @author Gal Lalouche
 */
public class Location {
	private static final double	EARTH_RADIUS	= 6371 * 1000;
	private static final double	MIN_LAT			= -PI / 2;
	private static final double	MAX_LAT			= PI / 2;
	private static final double	MIN_LON			= -PI;
	private static final double	MAX_LON			= PI;
	private final double		latitude;
	private final double		longitude;
	private final String		address;
	
	/**
	 * Creates a new location with no address
	 * 
	 * @param latitude The location's latitude coordinate
	 * @param longtitude The location's longitude coordinate
	 */
	public Location(double latitude, double longtitude) {
		this(latitude, longtitude, null);
	}
	
	/**
	 * Creates a new location
	 * 
	 * @param latitude The location's latitude coordinate
	 * @param longtitude The location's longitude coordinate
	 * @param address The address of the location
	 */
	public Location(double latitude, double longtitude, String address) {
		this.latitude = latitude;
		this.longitude = longtitude;
		this.address = address;
	}
	
	/**
	 * Creates a new location from a JSONObject
	 * 
	 * @param object The object to create from.
	 * @throws JSONException
	 */
	public Location(JSONObject object) throws JSONException {
		this(object.getDouble("lat"), object.getDouble("long"), object.optString("address", null));
	}
	
	private static double coordToRad(double coord) {
		return (PI * coord) / 180;
	}
	
	private static double radToCoord(double rad) {
		double rad2 = rad;
		if (rad2 > PI) {
			rad2 -= 2 * PI;
		}
		return (180 * rad2) / PI;
	}
	
	/**
	 * @return the latitude
	 */
	public double getLatitude() {
		return latitude;
	}
	
	/**
	 * @return the longitude
	 */
	public double getLongitude() {
		return longitude;
	}
	
	/**
	 * @return the address
	 */
	public String getAddress() {
		return address;
	}
	
	/**
	 * Returns the distance in meters between this and another location
	 * 
	 * @param other The location to get the distance to
	 * @return The distance to (in meters) from the other location
	 */
	public double getDistanceTo(Location other) {
		double myLat = coordToRad(this.latitude);
		double myLong = coordToRad(this.longitude);
		double otherLat = coordToRad(other.latitude);
		double otherLong = coordToRad(other.longitude);
		double result = acos((sin(myLat) * sin(otherLat)) + (cos(myLat) * cos(otherLat) * cos(myLong - otherLong)))
				* EARTH_RADIUS;
		return result;
	}
	
	/**
	 * Gets the bounding coordinates for the distance
	 * 
	 * @param distance The distance from the coordinate
	 * @return The NW, SE bounds of the bounding rectangle
	 */
	public Pair<Location, Location> getBoundingCoordinates(double distance) {
		if ((EARTH_RADIUS < 0d) || (distance < 0d)) {
			throw new IllegalArgumentException();
		}
		
		// angular distance in radians on a great circle
		double radDist = distance / EARTH_RADIUS;
		
		double minLat = coordToRad(this.latitude) - radDist;
		double maxLat = coordToRad(this.latitude) + radDist;
		
		double minLon, maxLon;
		if ((minLat > MIN_LAT) && (maxLat < MAX_LAT)) {
			double deltaLon = Math.asin(Math.sin(radDist) / Math.cos(coordToRad(this.latitude)));
			minLon = coordToRad(this.longitude) - deltaLon;
			if (minLon < MIN_LON) {
				minLon += 2d * Math.PI;
			}
			maxLon = coordToRad(this.longitude) + deltaLon;
			if (maxLon > MAX_LON) {
				maxLon -= 2d * Math.PI;
			}
		} else {
			// a pole is within the distance
			minLat = Math.max(minLat, MIN_LAT);
			maxLat = Math.min(maxLat, MAX_LAT);
			minLon = MIN_LON;
			maxLon = MAX_LON;
		}
		
		return Pair.of(new Location(radToCoord(minLat), radToCoord(minLon)), new Location(radToCoord(maxLat),
				radToCoord(maxLon)));
	}
	
	@Override
	public String toString() {
		return MessageFormat.format("[({0},{1}), {2}]", latitude, longitude, address);
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = (prime * result) + ((address == null) ? 0 : address.hashCode());
		long temp;
		temp = Double.doubleToLongBits(latitude);
		result = (prime * result) + (int)(temp ^ (temp >>> 32));
		temp = Double.doubleToLongBits(longitude);
		result = (prime * result) + (int)(temp ^ (temp >>> 32));
		return result;
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		Location other = (Location)obj;
		if (address == null) {
			if (other.address != null) {
				return false;
			}
		} else if (!address.equals(other.address)) {
			return false;
		}
		if (Double.doubleToLongBits(latitude) != Double.doubleToLongBits(other.latitude)) {
			return false;
		}
		if (Double.doubleToLongBits(longitude) != Double.doubleToLongBits(other.longitude)) {
			return false;
		}
		return true;
	}
	
}
